﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;


using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.Configuration;
using System.Reflection;

using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.Administration;

namespace SPaDevToolkit.Administration
{
    public abstract class SPaWebServiceApplicationProxy<C> : SPIisWebServiceApplicationProxy
    {
        [Persisted]
        SPServiceLoadBalancer _loadBalancer;

        ChannelFactory<C> _channelFactory;
        object _channelFactoryLock = new object();
        string _endpointConfigurationName;

        #region Constructors
        public SPaWebServiceApplicationProxy()
        {
        }

        public SPaWebServiceApplicationProxy(string name, SPIisWebServiceProxy serviceProxy, Uri serviceEndpointUri)
            : base(name, serviceProxy, serviceEndpointUri)
        {
            _loadBalancer = new SPRoundRobinServiceLoadBalancer(serviceEndpointUri);
        }
        #endregion

        #region Overrides
        public override void Provision()
        {
            _loadBalancer.Provision();
            base.Provision();
        }

        public override void Unprovision(bool deleteData)
        {
            _loadBalancer.Unprovision();
            base.Unprovision(deleteData);
        }
        #endregion

        #region Properties
        protected SPServiceLoadBalancer LoadBalancer
        {
            get { return _loadBalancer; }
        }

        public bool IsServiceAvailable
        {
            get
            {
                PropertyInfo addressesProperty = typeof(SPServiceLoadBalancer).GetProperty("EndpointAddresses", BindingFlags.NonPublic);
                if (addressesProperty != null)
                {
                    IEnumerable<Uri> addresses = (IEnumerable<Uri>)addressesProperty.GetValue(_loadBalancer, null);
                    if (addresses != null)
                        return addresses.Count() > 0;
                }
                return true;
            }
        }
        #endregion

        protected abstract string ClientConfigurationPath { get; }

        #region Service Application Execution
        C GetChannel(Uri address, bool asProcess)
        {
            string endpointConfigurationName = GetEndpointConfigurationName(address);

            // Check for a cached channel factory for the endpoint configuration
            if (_channelFactory == null || endpointConfigurationName != _endpointConfigurationName)
            {
                lock (_channelFactoryLock)
                {
                    // Double check to be sure the channel factory will not be created twice
                    if (_channelFactory == null || endpointConfigurationName != _endpointConfigurationName)
                    {
                        // NOTE: It is not thread-safe to dispose the channel factory in use. We let the 
                        // GC clean up because changing the endpoint configuration is a rare operation.

                        _channelFactory = CreateChannelFactory<C>(endpointConfigurationName);
                        _endpointConfigurationName = endpointConfigurationName;
                    }
                }
            }

            if (asProcess)
                return _channelFactory.CreateChannelAsProcess<C>(new EndpointAddress(address));
            else
                return _channelFactory.CreateChannelActingAsLoggedOnUser<C>(new EndpointAddress(address));
        }

        ChannelFactory<T> CreateChannelFactory<T>(string endpointConfigurationName)
        {
            // Open the Client.config file from the WebClients folder
            string clientConfigurationPath = SPUtility.GetGenericSetupPath(ClientConfigurationPath);
            Configuration clientConfiguration = OpenClientConfiguration(clientConfigurationPath);
            ConfigurationChannelFactory<T> factory = new ConfigurationChannelFactory<T>(endpointConfigurationName, clientConfiguration, null);

            // Configure the channel factory for IDFx claims authentication
            factory.ConfigureCredentials(SPServiceAuthenticationMode.Claims);

            return factory;
        }

        string GetEndpointConfigurationName(Uri address)
        {
            if (address == null)
                throw new ArgumentNullException("address");

            if (address.Scheme == Uri.UriSchemeHttps)
                return "https";
            else if (address.Scheme == Uri.UriSchemeHttp)
                return "http";

            throw new NotSupportedException("Unsupported endpoint address");
        }

        protected T Execute<T>(string operationName, Func<C, T> operation) where T : class
        {
            if (Status != SPObjectStatus.Online)
                throw new InvalidOperationException(this.DisplayName + " is not online");

            T result = null;

            using (new SPMonitoredScope(this.TypeName + ".Execute:" + operationName))
            {
                SPServiceLoadBalancerContext loadBalancerContext = null;

                using (new SPMonitoredScope("LoadBalancerContext:BeginOperation"))
                    loadBalancerContext = _loadBalancer.BeginOperation();

                try
                {
                    IChannel channel;

                    using (new SPMonitoredScope("GetChannel:" + loadBalancerContext.EndpointAddress))
                        channel = (IChannel)GetChannel(loadBalancerContext.EndpointAddress, true);

                    try
                    {
                        using (new SPMonitoredScope("ExecuteServiceOperation"))
                            result = operation((C)channel);
                    }
                    finally
                    {
                        try
                        {
                            channel.Close();
                        }
                        catch (CommunicationObjectFaultedException)
                        {
                            channel.Abort();
                            throw;
                        }
                        catch (TimeoutException)
                        {
                            channel.Abort();
                            throw;
                        }
                    }
                }
                catch (Exception)
                {
                    if (loadBalancerContext != null)
                        loadBalancerContext.Status = SPServiceLoadBalancerStatus.Failed;

                    throw;
                }
                finally
                {
                    if (loadBalancerContext != null)
                    {
                        using (new SPMonitoredScope("LoadBalancerContext:EndOperation"))
                            loadBalancerContext.EndOperation();
                    }
                }
            }

            return result;
        }
        #endregion
    }
}
